-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: [FN-80] 소셜 로그인 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. ## Walkthrough
OAuth 콜백 엔드포인트에서 소셜 계정 연동과 소셜 로그인 처리를 분리하고, 예외 처리 및 로깅을 개선하였습니다. 소셜 로그인 성공 시 리프레시 토큰 쿠키를 설정하며, 관련 엔드포인트와 에러코드, 리포지토리, 클라이언트 경로, 시큐리티 설정이 확장되었습니다.
## Changes
| Cohort / File(s) | Change Summary |
|------------------|---------------|
| **OAuthController 리팩토링 및 콜백 분리**<br> `src/main/java/project/flipnote/auth/controller/OAuthController.java` | 콜백 메서드를 소셜 계정 연동과 소셜 로그인으로 분리, 예외별 리다이렉트 및 로깅, 리프레시 토큰 쿠키 설정, 헬퍼 메서드 다수 추가, 서비스 호출 방식 변경 등 대규모 리팩토링 |
| **OAuthService 기능 확장**<br> `src/main/java/project/flipnote/auth/service/OAuthService.java` | 소셜 로그인 토큰 발급 로직 추가, provider 검증 강화, state 토큰 관리 개선, OAuth 유저 정보 요청 로직 분리 등 |
| **에러코드 추가 및 수정**<br> `src/main/java/project/flipnote/auth/exception/AuthErrorCode.java` | ALREADY_LINKED_SOCIAL_ACCOUNT 상태 변경(CONFLICT), 미등록 소셜 계정 및 지원하지 않는 소셜 제공자 에러코드 추가 |
| **클라이언트 경로 확장**<br> `src/main/java/project/flipnote/common/config/ClientProperties.java`,<br> `src/main/resources/application.yml` | SOCIAL_LOGIN_SUCCESS, SOCIAL_LOGIN_FAILURE 경로 및 설정 추가 |
| **OAuthApiClient 개선**<br> `src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java` | state 파라미터가 null일 경우 쿼리에서 제외하도록 URI 빌더 로직 수정 |
| **UserOAuthLinkRepository 확장**<br> `src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java` | provider, providerId로 user까지 fetch join하는 쿼리 메서드 추가 |
| **시큐리티 설정 보완**<br> `src/main/java/project/flipnote/common/security/config/SecurityConfig.java` | `/oauth2/authorization/{provider}` GET 요청도 인증 없이 허용하도록 변경 |
## Sequence Diagram(s)
```mermaid
sequenceDiagram
participant Client
participant OAuthController
participant OAuthService
participant OAuthApiClient
participant UserOAuthLinkRepository
participant JwtComponent
Client->>OAuthController: GET /oauth2/callback/{provider}?code=...&state=...
OAuthController->>OAuthController: state 파라미터 체크
alt state 존재 (소셜 연동)
OAuthController->>OAuthService: linkSocialAccount(...)
OAuthService->>OAuthApiClient: getAccessToken + getUserInfo
OAuthService->>UserOAuthLinkRepository: 계정 연동 처리
OAuthController-->>Client: 연동 성공/실패 리다이렉트
else state 없음 (소셜 로그인)
OAuthController->>OAuthService: socialLogin(...)
OAuthService->>OAuthApiClient: getAccessToken + getUserInfo
OAuthService->>UserOAuthLinkRepository: 연동된 계정 조회
OAuthService->>JwtComponent: 토큰 페어 발급
OAuthController-->>Client: 로그인 성공 리다이렉트 (액세스 토큰 쿼리, 리프레시 쿠키)
endEstimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Possibly related PRs
Suggested reviewers
Poem
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/main/java/project/flipnote/auth/controller/OAuthController.java(3 hunks)src/main/java/project/flipnote/auth/exception/AuthErrorCode.java(1 hunks)src/main/java/project/flipnote/auth/service/OAuthService.java(5 hunks)src/main/java/project/flipnote/common/config/ClientProperties.java(1 hunks)src/main/java/project/flipnote/common/security/config/SecurityConfig.java(1 hunks)src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java(1 hunks)src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java(2 hunks)src/main/resources/application.yml(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/project/flipnote/auth/exception/AuthErrorCode.java (2)
src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)src/main/java/project/flipnote/auth/constants/AuthRedisKey.java (1)
AuthRedisKey(7-21)
src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java (2)
src/main/java/project/flipnote/user/entity/UserOAuthLink.java (2)
UserOAuthLink(20-52)UserOAuthLink(46-51)src/main/java/project/flipnote/user/entity/User.java (1)
User(20-105)
🔇 Additional comments (12)
src/main/resources/application.yml (1)
65-66: 소셜 로그인 경로 설정이 올바르게 추가되었습니다.새로운 클라이언트 경로 설정이 기존 소셜 링크 경로와 일관성 있게 구성되어 있으며, OAuth 컨트롤러에서 사용할 리다이렉트 URL을 제공합니다.
src/main/java/project/flipnote/common/config/ClientProperties.java (1)
22-23: PathKey enum에 소셜 로그인 관련 상수가 적절히 추가되었습니다.새로운 상수들이 기존 네이밍 컨벤션을 따르고 있으며,
application.yml의 설정과 일치합니다.src/main/java/project/flipnote/common/security/config/SecurityConfig.java (1)
69-69: OAuth 인증 엔드포인트에 대한 접근 권한이 올바르게 설정되었습니다.
/oauth2/authorization/{provider}엔드포인트를 인증 없이 허용하도록 설정한 것은 OAuth 플로우에서 표준적인 구성입니다.src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java (1)
81-94: 인증 URI 생성 로직이 개선되었습니다.State 파라미터를 조건부로 추가하는 방식으로 변경하여 null 값이 URI에 포함되는 것을 방지했습니다. 이는 소셜 로그인과 소셜 계정 연동의 서로 다른 플로우를 지원하는 좋은 개선사항입니다.
src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java (1)
20-30: 효율적인 JOIN FETCH 쿼리가 잘 구현되었습니다.
findByProviderAndProviderIdWithUser메서드는 OAuth 링크와 연관된 사용자 엔티티를 한 번의 쿼리로 조회하는 효율적한 구현입니다. 소셜 로그인 플로우에서 N+1 문제를 방지할 수 있습니다.src/main/java/project/flipnote/auth/exception/AuthErrorCode.java (1)
24-25: 적절한 오류 코드 추가입니다.소셜 로그인 기능에 필요한 오류 코드들이 적절한 HTTP 상태 코드와 명확한 메시지로 추가되었습니다.
src/main/java/project/flipnote/auth/service/OAuthService.java (4)
46-57: 메서드 시그니처 개선이 잘 되었습니다.
UserAuth객체를 사용하여 인증된 사용자와 미인증 사용자 모두를 처리할 수 있도록 개선되었습니다.
93-101: 소셜 로그인 구현이 명확합니다.OAuth 사용자 정보 조회, 연동 계정 확인, JWT 토큰 생성의 흐름이 잘 구성되었습니다.
111-117: Provider 검증 로직이 개선되었습니다.Optional을 사용한 null 안전성과 명확한 로깅이 추가되어 디버깅이 용이해졌습니다.
119-126: State 생성 로직이 잘 분리되었습니다.인증 여부에 따라 state 토큰 생성을 조건부로 처리하여 소셜 연동과 소셜 로그인 플로우를 명확히 구분합니다.
src/main/java/project/flipnote/auth/controller/OAuthController.java (2)
65-70: 콜백 처리 플로우가 명확하게 분리되었습니다.state 파라미터 유무로 소셜 연동과 소셜 로그인을 구분하는 로직이 간결하고 이해하기 쉽습니다.
125-132: 리프레시 토큰 쿠키 생성이 적절합니다.JWT 설정의 만료 시간을 사용하여 쿠키를 생성하는 로직이 잘 구현되었습니다.
| ResponseCookie refreshTokenCookie = null; | ||
| try { | ||
| TokenPair tokenPair = oAuthService.socialLogin(provider, code, codeVerifier, request); | ||
| location = buildLoginSuccessRedirectUri(tokenPair.accessToken()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
보안 우려: Access Token이 URL에 노출됩니다.
Access Token을 쿼리 파라미터로 전달하면 서버 로그, 브라우저 히스토리, Referer 헤더 등에 노출될 위험이 있습니다.
더 안전한 방법을 고려해보세요:
- Fragment identifier (#) 사용
- 임시 코드 발급 후 클라이언트에서 토큰 교환
- Secure cookie 사용
🌐 Web query:
What are the best practices for passing JWT access tokens to client applications after OAuth login?
💡 Result:
The best practices for passing JWT access tokens to client applications after OAuth login focus on minimizing security risks while ensuring tokens are used as intended. Critical considerations involve secure transmission, proper storage, token content, and lifecycle management.
Key Best Practices:
-
Always send JWTs over HTTPS. Never transmit tokens over unsecured (HTTP) channels, as interception would allow attackers to use the token to access protected resources[2][4][7].
-
Deliver access tokens using the Authorization header. After login, provide the JWT to the client, which should then store it temporarily in memory or as securely as possible and send it back in the
Authorization: Bearer <token>header with each API request[7][9]. -
Limit storage risk:
- Web apps: Prefer storing JWTs in HttpOnly, Secure cookies to mitigate XSS exposure. Avoid localStorage and sessionStorage for access tokens, as XSS can expose these[1].
- Mobile apps: Store tokens using secure platform-specific methods only if absolutely necessary and for very short duration—ideally, keep them in volatile memory when possible[6].
- Single-page apps (SPA): Short-lived tokens in memory are safest, but refresh tokens and persistent storage require careful risk analysis and controls[1][6].
-
Use short-lived access tokens. Set a brief expiration time (commonly 5–15 minutes). Use refresh tokens—secured as tightly as possible—to obtain new access tokens without re-authenticating the user[1][2][3][6].
-
Minimize token content. Only include claims strictly necessary for identity and authentication (e.g.,
user_id,org_id). Do not include permissions, sensitive data, or information that changes frequently—authorization logic should be handled by backend or a dedicated authorization system[2][7]. -
Validate every token on every request.
- Always check signature validity, expiration (
expclaim), audience (aud), issuer (iss), and intended use of the token[1][3][7]. - Never trust token claims without verification by the server[1][7].
- Always check signature validity, expiration (
-
Avoid passing tokens in URLs. Never place JWTs in query strings or URL fragments, as they can leak via logs, browser history, or referrer headers[1][10].
-
Rotate keys periodically. Update signing keys frequently and allow a short overlap period to prevent user lockout during key transitions[4].
-
Separate tokens by use. Prefer using different secrets/keys for access, refresh, and ID tokens, and make sure only the correct systems can sign and verify them[1][5].
Example Secure Flow:
- After OAuth login, the backend issues a JWT access token (plus a refresh token, if applicable).
- The backend transmits the JWT to the client via HTTPS.
- The client stores the access token in memory or a secure, short-lived store (e.g., an HttpOnly cookie for web apps).
- The client includes the JWT in the
Authorization: Bearer <token>header for subsequent API requests. - When the access token expires, the client uses the refresh token (kept securely) to obtain a new access token.
- The backend/authorization server validates each token on every request.
Caveats:
- Storing tokens on the client always carries some risk. XSS attacks can expose tokens in localStorage/sessionStorage. Cookies help but may be vulnerable to CSRF if not configured with
SameSite=Strict[1][6]. - Never use JWTs as persistent sessions. Use them as short-lived proofs of authentication/authorization only[1][2].
- If handling sensitive data or operating in environments with serious security requirements (e.g., banking apps), follow stricter controls such as avoiding direct JWT storage and using opaque tokens with backend introspection[6].
By following these practices, you minimize both the risk of token compromise and the potential damage if a token is stolen or misused.
Citations:
- 1: https://www.aptori.com/blog/jwt-security-best-practices-every-developer-should-know
- 2: https://www.permit.io/blog/how-to-use-jwts-for-authorization-best-practices-and-common-mistakes
- 3: https://curity.io/resources/learn/jwt-best-practices/
- 4: https://workos.com/blog/oauth-and-jwt-how-to-use-and-best-practices
- 5: https://www.vaadata.com/blog/jwt-json-web-token-vulnerabilities-common-attacks-and-security-best-practices/
- 6: https://curity.io/resources/learn/oauth-for-mobile-apps-best-practices/
- 7: https://auth0.com/docs/secure/tokens/token-best-practices
- 8: https://blog.convisoappsec.com/en/secure-api-development-best-practices-oauth2-and-jwt/
- 9: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
- 10: https://community.auth0.com/t/difference-between-sending-oauth-codes-and-jwt-tokens-in-url-query-security/106312
보안 우려: Access Token URL 노출 방지 필요
Access Token을 쿼리 파라미터로 전달하면 서버 로그, 브라우저 히스토리, Referer 헤더 등에 토큰이 노출될 수 있습니다. 아래와 같이 안전한 전달 방법으로 변경하세요.
수정 필요 위치
- src/main/java/project/flipnote/auth/controller/OAuthController.java:104
권장 대체 방안
- HttpOnly, Secure 속성의 쿠키에 토큰 저장
- 클라이언트가
Authorization: Bearer <token>헤더로 전송 - OAuth 임시 인가 코드(authorization code) 발급 후 클라이언트에서 토큰 교환
예시 패치
- location = buildLoginSuccessRedirectUri(tokenPair.accessToken());
+ // 1) 쿠키에 저장하고 리디렉션 URI에는 토큰을 포함하지 않음
+ String redirectUri = buildLoginSuccessRedirectUri(null);
+ ResponseCookie cookie = ResponseCookie.from("access_token", tokenPair.accessToken())
+ .httpOnly(true).secure(true).path("/").maxAge(accessTokenTtl).build();
+ response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
+ location = redirectUri;—or—
- location = buildLoginSuccessRedirectUri(tokenPair.accessToken());
+ // 2) URL fragment 방식을 사용해 쿼리 파라미터에 노출되지 않도록 변경
+ String fragment = "#access_token=" + tokenPair.accessToken();
+ location = buildLoginSuccessRedirectUri(fragment);위 중 하나를 선택해 구현하시고, 클라이언트에서도 해당 방식에 맞춰 토큰을 안전하게 처리하도록 변경해 주세요.
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/auth/controller/OAuthController.java at line
104, the access token is currently being included in the URL query parameters,
which exposes it to security risks. To fix this, remove the token from the URL
and instead store it securely in an HttpOnly, Secure cookie set by the server,
or implement an OAuth authorization code flow where the client exchanges a
temporary code for the token. Update the server code to set the token in a
secure cookie or issue an authorization code, and ensure the client is modified
to send the token in the Authorization header or handle the code exchange
accordingly.
📝 변경 내용
✅ 체크리스트
💬 기타 참고 사항
Summary by CodeRabbit
신규 기능
버그 수정
기타